/*
 * Copyright 2012 Finbudget.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package finbudgettable.client.ui.grid.dnd;

import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.*;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;
import finbudgettable.client.ui.grid.cell.BrowserEventListener;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class DragHelper<C> implements BrowserEventListener<C> {

    interface Resources extends ClientBundle {

        interface Style extends CssResource {

            @ClassName("b-dropzone-active")
            String bDropzoneActive();

            @ClassName("b-dropzone")
            String bDropzone();

            @ClassName("b-dragging-column")
            String bDraggingColumn();
        }

        @Source("dnd.css")
        Style style();
    }

    private static final int SENSITIVITY = 5;
    static final Resources RESOURCES = GWT.create(Resources.class);

    static {
        RESOURCES.style().ensureInjected();
    }

    @SuppressWarnings("rawtypes")
	private final Draggable draggable;

    private int firstTouchX;
    private int firstTouchY;
    private com.google.gwt.user.client.Element draggingElement;
    private final DragAndDropController controller;

    private final Set<String> consumedEvents =
            new HashSet<String>(Arrays.asList(
                    BrowserEvents.MOUSEDOWN,
                    BrowserEvents.MOUSEMOVE,
                    BrowserEvents.MOUSEUP));


    public DragHelper(@SuppressWarnings("rawtypes") Draggable draggable, DragAndDropController controller) {
        this.draggable = draggable;
        this.controller = controller;
    }

    public static com.google.gwt.user.client.Element cloneElement(Element element, int x, int y) {
        com.google.gwt.user.client.Element clone = element.cloneNode(true).cast();
        clone.addClassName(RESOURCES.style().bDraggingColumn());
        clone.getStyle().setLeft(x, Style.Unit.PX);
        clone.getStyle().setTop(y, Style.Unit.PX);
        Document.get().getBody().appendChild(clone);
        return clone;
    }

    private final EventListener draggingEventListener = new EventListener() {
        @Override
        public void onBrowserEvent(Event event) {
            String eventType = event.getType();
            if (BrowserEvents.MOUSEMOVE.equals(eventType)) {
                int x = event.getClientX() + Document.get().getScrollLeft();
                int y = event.getClientY() + Document.get().getScrollTop();
                draggingElement.getStyle().setLeft(x, Style.Unit.PX);
                draggingElement.getStyle().setTop(y, Style.Unit.PX);
                controller.fireDragEvent(draggable, x, y);

            } else if (BrowserEvents.MOUSEUP.equals(eventType)) {
                DOM.releaseCapture(draggingElement);
                DOM.setEventListener(draggingElement, null);
                Document.get().getBody().removeChild(draggingElement);
                controller.fireDragEndEvent(draggable);
            }
        }
    };

    private final EventListener eventListener = new EventListener() {
        @Override
        public void onBrowserEvent(Event event) {
            String eventType = event.getType();
            if (BrowserEvents.MOUSEMOVE.equals(eventType)) {
                if (isMoveEnough(event)) {
                    DOM.releaseCapture(draggingElement);
                    DOM.setEventListener(draggingElement, null);

                    int width = draggingElement.getAbsoluteRight() - draggingElement.getAbsoluteLeft();
                    int height = draggingElement.getAbsoluteBottom() - draggingElement.getAbsoluteTop();

                    draggingElement = cloneElement(draggingElement, draggingElement.getAbsoluteLeft(), draggingElement.getAbsoluteTop());
                    draggingElement.getStyle().setWidth(width, Style.Unit.PX);
                    draggingElement.getStyle().setHeight(height, Style.Unit.PX);
                    DOM.setEventListener(draggingElement, draggingEventListener);
                    int events = Event.getTypeInt(BrowserEvents.MOUSEMOVE) | Event.getTypeInt(BrowserEvents.MOUSEUP);
                    DOM.sinkEvents(draggingElement, events);
                    DOM.setCapture(draggingElement);
                    controller.fireDragStartEvent(draggable);
                }
            } else if (BrowserEvents.MOUSEUP.equals(eventType)) {
                DOM.releaseCapture(draggingElement);
                DOM.setEventListener(draggingElement, null);
            }
        }

        private boolean isMoveEnough(Event event) {
            return Math.max(Math.abs(firstTouchX - event.getClientX()), Math.abs(firstTouchY - event.getClientY())) >= SENSITIVITY;
        }
    };

    @Override
    public boolean onBrowserEvent(Cell.Context context, Element parent, C value,
                                  NativeEvent event, ValueUpdater<C> valueUpdater) {
        Element draggable = this.draggable.getDraggableElement();
        if (event.getEventTarget().equals(draggable)) {
            String eventType = event.getType();
            if (BrowserEvents.MOUSEDOWN.equals(eventType)) {
                boolean mouseDown = event.getButton() == NativeEvent.BUTTON_LEFT;
                if (mouseDown) {
                    firstTouchX = event.getClientX();
                    firstTouchY = event.getClientY();
                    draggingElement = draggable.cast();
                    DOM.setEventListener(draggingElement, eventListener);
                    DOM.setCapture(draggingElement);
                    event.preventDefault();
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public Set<String> getConsumedEvents() {
        return consumedEvents;
    }
}
